# useState
import { ref } from '@vue/reactivity'
const useState = function (initialState) {
const state = ref(initialState)
const setState = (value) => {
// console.log(value)
state.value = value
}
return [state, setState]
}
export default useState
2
3
4
5
6
7
8
9
10
11
# defineOptions自定义组件名
defineOptions({
name: 'MyIndex'
})
2
3
# defineProps
defineProps解构丢失响应式的问题
解决办法:
const props = defineProps<Props>();
console.log(props.id)
2
从Vue3.5开始,从 defineProps 返回值解构出的变量是响应式的 => 详见官方文档 (opens new window)
# withDefaults
withDefaults(props: defineProps, defaults: Record<string, any>)
# defineEmits
使用 defineEmits 函数,你可以明确声明组件可以触发的事件
<script setup lang="ts">
import { defineEmits } from 'vue';
// 使用 defineEmits 来定义组件可以触发的自定义事件
const emit = defineEmits(['update:first', 'update:second']);
// 在组件的逻辑中触发自定义事件
const handleClick = () => {
emit('update:first', '这是 update:first 事件的数据');
emit('update:second', '这是 update:second 事件的数据');
};
</script>
2
3
4
5
6
7
8
9
10
11
12
类型声明
type TestEvent = {
'update:info': [val?: any]
(e: 'update:info', val?: any): any
}
2
3
4
# defineModel
我们知道v-model是 :value和@input的语法糖
<input type="text" v-model="localeId" />
<input type="text" :value="localeId" @input="localeId = $event.target.value" />
2
上面两段代码是等效的
而defineModel也是个语法糖
defineModel 是对 props 和 emit 的封装,等价于以下代码:
<template>
<input type="text" :value="modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
defineProps({
modelValue: {
type: String,
default: 'Default Value',
},
});
const emit = defineEmits(['update:modelValue']);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
亦或是
<template>
<p>id <input type="text" v-model="id2" /></p>
</template>
<script setup lang="ts">
defineProps({
modelValue: {
type: String,
default: 'Default Value',
},
});
const emit = defineEmits(['update:modelValue']);
const localValue = ref(props[modelValue]); // 使用响应式变量绑定父组件数据
watch(localValue, (newValue) => {
emit(`update:modelValue`, newValue); // 子组件更新 -> 父组件同步
});
// 这里也可以用computed
const localeId = computed({
get() {
return props.id
},
set(val) {
emit('update:id', val)
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
编写一个defineModel函数
function defineModel(name = 'modelValue') {
const props = defineProps({ [name]: Object });
const emit = defineEmits([`update:${name}`]);
const localValue = ref(props[name]); // 使用响应式变量绑定父组件数据
watch(() => props[name], (newValue) => {
localValue.value = newValue; // 父组件更新 -> 子组件同步
});
watch(localValue, (newValue) => {
emit(`update:${name}`, newValue); // 子组件更新 -> 父组件同步
});
return localValue;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defineModel接收两个参数
第一个参数是绑定的模型名称,第二个参数是配置对象
defineModel返回的是一个Ref对象CustomRefImpl
set 函数必须返回一个值。这个返回值将是 isDeepThink 的最终新值,并且也会是发出 update:isDeepThink 事件时传递给父组件的值。
const id = defineModel('id', {
type: Number,
// default: 100,
required: true,
set(v) {
return v
}
})
2
3
4
5
6
7
8
需要注意的是:default和set不能同时设置
# defineModel的应用
defineModel特别适用于ElDialog等组件的visible绑定
# v-memo
https://cn.vuejs.org/api/built-in-directives.html#v-memo
类似于useMemo,缓存一个组件,当依赖不变时,组件内部将不会更新。
<div v-memo="[valueA, valueB]">
...
</div>
2
3
搭配 v-for 使用 v-memo,确保两者都绑定在同一个元素上。v-memo 不能用在 v-for 内部。
# h渲染函数
Vue3将$createElement从this上移除了,改为使用h
<script setup lang="ts">
import { h } from 'vue'
const renderFunc = () => h('div', 'xxx')
</script>
<template>
<component :is="renderFunc"></component>
</template>
2
3
4
5
6
7
8
9
使用h自定义组件
<template>
<RenderTask></RenderTask>
</template>
<script setup lang="ts">
const RenderTask = () => h('div', {
onClick: () => {
console.log(123);
}
},'123');
</script>
2
3
4
5
6
7
8
9
10
11
# shallowRef
当ref的参数是非常大且具有深层次嵌套的数组对象时,可能会导致性能问题。
使用 shallowRef: 如果您有一个数组对象,而不需要追踪对象属性的变化,可以使用 shallowRef 而不是 ref。shallowRef 只会追踪对象本身的变化,而不会深度追踪嵌套对象的属性。
# shallowReactive
shallowReactive 只追踪对象或数组的顶层属性变化,不会递归处理内部嵌套的对象属性,因此对于数组来说,它会对增删操作进行响应式处理,但不会对数组中对象的属性变化产生响应。
import { shallowReactive, watch } from 'vue';
// 使用 shallowReactive 创建一个浅层响应式数组
const data = shallowReactive([{ id: 1 }, { id: 2 }]);
// 监听数组的长度变化
watch(
() => data.length, // 监听数组的长度属性
(newLength, oldLength) => {
console.log('Array length changed:', newLength, oldLength);
}
);
// 增加一条数据
data.push({ id: 3 }); // 触发 watch,因为数组长度发生了变化
// 删除一条数据
data.pop(); // 触发 watch,因为数组长度发生了变化
// 修改对象内部属性
data[0].id = 99; // 不会触发 watch,因为 shallowReactive 不递归监听内部属性
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# shallowReactive和shallowRef
基本定义:
shallowReactive:创建一个浅层响应式对象,对象的顶层属性是响应式的,但不会递归地处理内部嵌套的属性。shallowRef:创建一个浅层的ref,仅对值本身进行响应式处理,而不会递归处理嵌套对象的内部属性。
使用场景:
shallowReactive:用于需要响应顶层属性变化的对象或数组场景,尤其是当对象或数组内部有大量嵌套数据时,可以避免不必要的深层追踪。例如,监听对象的增删、数组的增删,而忽略内部属性。shallowRef:适用于只需要响应整体值变化的场景,例如,完全替换对象或数组,而不需要对内部属性的变化产生反应。
# watch
侦听一个 getter 函数:
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
2
3
4
5
6
7
侦听一个 ref:
const count = ref(0)
watch(count, (newVal, oldVal) => {
/* ... */
}, {
deep: true,
immediate: true
})
2
3
4
5
6
7
侦听一组数据
const count = ref<number>(100);
const count1 = ref<number>(200);
watch([count, count1], (newVal, oldVal) => {
console.log('newVal: ', newVal, oldVal);
});
2
3
4
5
6
# watchEffect (opens new window)
# watchEffect与watch❓
watchEffect不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行,而watch只能监听指定的属性而做出变更(v3开始可以同时指定多个)。第二点就是 watch 可以获取到新值与旧值(更新前的值),而
watchEffect是拿不到的。watch:{ someValue(newValue,oldValue){ // ... } }1
2
3
4
5第三点是 watchEffect 如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与
computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而 watch 不需要,因为他一开始就指定了依赖。
# computed
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
2
3
4
5
6
7
computed函数的第二个参数 debugOptions 包含俩个回调函数
onTrack将在响应属性或引用作为依赖项被跟踪时被调用。onTrigger将在侦听器回调被依赖项的变更触发时被调用。
# toRef和toRefs
当对一个reactive对象解构时,其内的属性会丢失响应性(如果属性是一个对象,则不会丢失响应性,除非继续解构)
const state = reactive({
count: 1,
message: 'Hello',
obj: {
count: 100
}
});
const {
count,
obj: { count: count1 }
} = state;
2
3
4
5
6
7
8
9
10
11
12
这个时候,使用toRefs包裹reactive对象,则解构出来的属性不会丢失响应性,解构出来的每个属性都是一个ref
const { count } = toRefs(state); // count: Ref<number>
而toRef则是针对某一个属性解构
const count = toRef(state, 'count');
# effectScope
effectScope 的作用是创建一个副作用作用域,用于隔离和管理副作用代码
const scope = effectScope();
scope.run(() => {
onMounted(() => {
console.log('mounted');
});
onBeforeUnmount(() => {
console.log('beforeunmount');
});
});
2
3
4
5
6
7
8
9
10
# provide/inject
provide('globalCount', 1000);
const globalData = inject('globalCount') // 1000
2
3
provide/inject的封装使用
这里将store设置为只读类型,以方便控制全局状态的修改
import { inject, provide, reactive, readonly } from 'vue'
type StoreType = {
name: string
age: number
}
type ContextType = {
state: Readonly<StoreType>
incrementAge: () => void
}
export function ProvideContext() {
const __state = reactive<StoreType>({
name: 'zs',
age: 20
})
const incrementAge = () => {
__state.age++
}
provide('GlobalContext', { state: readonly(__state), incrementAge })
}
export function useContext() {
const context = inject<ContextType>('GlobalContext')
if (!context) {
throw new Error('useContext must be used within a ProvideContext!')
}
return context
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
使用:
// 父级组件比如App.vue
ProvideContext()
// 子组件
const globalContext = useContext()
2
3
4
5
provide('trainType', computed(() => props.trainType)) 和 provide('trainType', props.trainType) 的区别
| 写法 | 是否响应 props.trainType 的变化 |
|---|---|
provide('trainType', props.trainType) | ❌ 不会变,值被“拍扁成普通值” |
provide('trainType', computed(() => props.trainType)) | ✅ 会跟随 props.trainType 变化 |
一个是响应式一个是普通值
# Vue3中的context使用
在使用inject接收响应式变量时,不能够解构,否则会丢失响应式
JS解构的本质是用新变量接收对象的值,并没有保留原始数据的引用,所以会丢失响应式
const context = reactive({
name: '',
age: 20
})
const {name,age} = useFormContext() // 不能解构,会丢失响应式
2
3
4
5
6
如果provide提供的是多个响应式变量的集合,那么此时可以使用解构,因为解构出来的还是响应式对象
const formModel = reactive({})
const trainParams = ref([])
const {formModel,trainParams} = useFormContext()
2
3
4
# Plugin
插件可以是一个带 install() 方法的对象,亦或直接是一个将被用作 install() 方法的函数。插件选项 (app.use() 的第二个参数) 将会传递给插件的 install() 方法。
import { inject, type App, type ObjectPlugin } from 'vue'
const myPlugin: ObjectPlugin<{ message: string }> = {
install(app: App, options) {
console.log('app: ', app)
// 不推荐
app.config.globalProperties.$myPluginMethod = () => {
console.log('myPluginMethod')
}
// 推荐
const myPluginMethod = () => {
console.log('myPluginMethod', options.message)
}
app.provide('myPluginMethod', myPluginMethod)
}
}
export function useMyPlugin() {
const myPluginMethod = inject<() => void>('myPluginMethod')
if (!myPluginMethod) {
throw new Error('myPlugin is not provided. Make sure to use app.use(myPlugin)!')
}
return { myPluginMethod }
}
export default myPlugin
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
使用
// main.ts
app.use(myPlugin, { message: 'hello' })
// Component
const { myPluginMethod } = useMyPlugin()
2
3
4
5
函数式写法:
export const ourPlugin = (options: { message: string }) => {
return {
install(app: App) {
console.log('app: ', app)
const ourPluginMethod = () => {
console.log('ourPluginMethod', options.message)
}
app.provide('ourPluginMethod', ourPluginMethod)
}
}
}
export function useOurPlugin() {
const ourPluginMethod = inject<() => void>('ourPluginMethod')
if (!ourPluginMethod) {
throw new Error('ourPlugin is not provided. Make sure to use app.use(ourPlugin)!')
}
return { ourPluginMethod }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用
// main.ts
app.use(ourPlugin({ message: 'hello' }))
2
# TypeScript 对 Vue 插件类型推断的一点小坑
在定义插件时,发现类型提示总是失效
/**
* Vue3 埋点插件
*/
export const TrackerPlugin = {
install(app: App, config: TrackerConfig) {
// 创建 Tracker 实例
const tracker = createTracker(config)
// 注入全局属性
app.config.globalProperties.$tracker = tracker
// 提供依赖注入
app.provide('tracker', tracker)
// 自动追踪路由变化(如果使用了 Vue Router)
if (config.enabled !== false) {
const router = app.config.globalProperties.$router
if (router) {
router.afterEach((to: any) => {
tracker.track('page_view', {
path: to.path,
name: to.name,
query: to.query
})
})
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
main.ts使用
app.use(TrackerPlugin, {
enabled: true,
reportUrl: '',
// ...
})
2
3
4
5
每次输完reportUrl后,后面的属性类型提示就失效了
经查询原来是:
TypeScript 对
Plugin泛型的推断在app.use调用时并不会把它和第二个参数的类型完全绑定,所以在main.ts写app.use(TrackerPlugin, {...})时,TS 无法推断config的类型,导致写完reportUrl后,类型提示就消失了。
解决方案是尽量把插件写成返回 Plugin 的工厂函数
export function createTrackerPlugin(config: TrackerConfig): Plugin {
return {
install(app: App) {
const tracker = createTracker(config)
app.config.globalProperties.$tracker = tracker
app.provide('tracker', tracker)
if (config.enabled !== false) {
const router = app.config.globalProperties.$router
if (router) {
router.afterEach((to: any) => {
tracker.track('page_view', {
path: to.path,
name: to.name,
query: to.query,
})
})
}
}
},
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
总结:Vue3 插件类型推断对 Plugin<T> 在 app.use 中有限支持,用插件工厂函数(方案 1)是目前推荐的做法。
# Directive自定义指令
https://cn.vuejs.org/guide/reusability/custom-directives.html#introduction
<script setup>
const vColor: Directive<any, string> = {
beforeMount: (el: HTMLDivElement, binding, vnode, prevVNode) => {
console.log('v-color', el, binding)
el.style.color = binding.value
}
}
</script>
2
3
4
5
6
7
8
export type Directive<T = any, V = any> = ObjectDirective<T, V> | FunctionDirective<T, V>;
export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (el: T, binding: DirectiveBinding<V>, vnode: VNode<any, T>, prevVNode: Prev) => void;
2
Directive泛型有两个参数,第一个是el的类型,即dom类型;第二个是自定义指令参数的类型,即binding.value的类型
全局自定义指令
// ObjectDirective
app.directive('bgcolor', {
beforeMount: (el: HTMLDivElement, binding) => {
el.style.backgroundColor = binding.value;
}
});
// FunctionDirective
app.directive('bg-color', (el: HTMLDivElement, binding, vnode, prevVNode) => {
el.style.backgroundColor = binding.value;
});
2
3
4
5
6
7
8
9
10
# 编写的自定义指令如何添加编辑器提示
新建types/directive.d.ts文件,声明合并
import type { FunctionDirective, ObjectDirective } from 'vue'
declare module 'vue' {
interface ComponentCustomProperties {
vBgColor: ObjectDirective<any, string>
vColor: FunctionDirective<any, string>
}
}
2
3
4
5
6
7
8
# Suspense
<script setup lang="ts">
const AsyncComponent = defineAsyncComponent(() =>
loadWithDelay(import('./pages/AsyncComponent.vue'), 2000)
)
const showAsyncComponent = ref<boolean>(false)
</script>
<template>
<button @click="showAsyncComponent = !showAsyncComponent">show</button>
<Suspense>
<template #fallback>Loading...</template>
<template #default>
<async-component></async-component>
</template>
</Suspense>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面的代码中,每次进入页面都会延迟加载2s再显示AsyncComponent组件,这是不符合我们预期的,我们希望的是除第一次进入页面加载延迟外后续能够立刻加载AsyncComponent,就像React那样。
为什么与预期结果不符呢?
这是因为React 的 Suspense 与 Vue 的异步组件加载机制不同
在 React 中,React.lazy 和 Suspense 会利用 React 的内部缓存机制,确保组件加载一次后,下次直接复用已经加载过的模块,避免了每次都重新加载异步组件。换句话说,React 的 Suspense 会确保异步组件在第一次加载后保持在内存中,之后的渲染直接使用已经加载的模块。
而Vue 的 Suspense 没有内建的缓存机制,每次渲染时都会触发异步组件的加载,除非你显式地进行缓存管理。
好了,那么我们要做的事情就很明确了,就是缓存已加载的异步组件。
这里是通过loadWithDelay模拟延迟的,来看代码
export const loadWithDelay = (promise: Promise<any>, time: number) => {
const delay = (d: number) => new Promise((resolve) => setTimeout(resolve, d))
const delayPromise = delay(time)
return Promise.all([promise, delayPromise]).then(() => promise)
}
2
3
4
5
添加缓存逻辑:
const cachedAsyncComponent = new Map()
export const loadWithDelay = (promise: Promise<any>, time: number, key: string) => {
if (cachedAsyncComponent.has(key)) {
return cachedAsyncComponent.get(key)
}
const delay = (d: number) => new Promise((resolve) => setTimeout(resolve, d))
const delayPromise = delay(time)
cachedAsyncComponent.set(key, promise)
return Promise.all([promise, delayPromise]).then(() => promise)
}
2
3
4
5
6
7
8
9
10
11
# defineAsyncComponent定义异步组件
在vue2中可以通过
<script>
const AsyncComponent = () =>
import(/* webpackChunkName: "AsyncComponent" */ '../components/AsyncComponent.vue')
</script>
<template>
<button @click="asyncComponentShow = true">加载异步组件</button>
<async-component v-if="asyncComponentShow"></async-component>
</template>
2
3
4
5
6
7
8
9
来导入一个异步组件,并在需要时使用
而在Vue3中,导入异步组件需要使用defineAsyncComponent函数
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
并且在使用时,外部需要包裹Suspense
<template>
<div>
<Suspense>
<AsyncComponent />
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</div>
</template>
2
3
4
5
6
7
8
9
10
# 使用defineAsyncComponent定义的组件没有编辑器类型提示
解决方案:显式地声明类型
const PDFViewer: DefineComponent<{ fileUrl: string }> = defineAsyncComponent(() =>
loadWithDelay(import('./PDFViewer.vue'), 2000, 'PDFViewer')
)
// @ts-ignore
const AsyncComponent: DefineSetupFnComponent<AsyncComponentProps, AsyncComponentEmits> = defineAsyncComponent(
() => loadWithDelay(import('../components/AsyncComponent.vue'), 2000, 'AsyncComponent')
)
const Await: (typeof import('../components/Await.vue'))['default'] = defineAsyncComponent(
() => import('../components/Await.vue')
)
2
3
4
5
6
7
8
9
10
11
12
13
# 动态加载异步组件没有显示fallback内容
<template>
<p>
<button @click="showAsyncComponent = !showAsyncComponent">showAsyncComponent</button>
<Suspense>
<template #fallback>Loading number...</template>
<async-component v-if="showAsyncComponent" :id="123"></async-component>
</Suspense>
</p>
</template>
<script setup lang="ts" async>
import { defineAsyncComponent, ref } from 'vue'
import { loadWithDelay } from '@/router'
defineOptions({ name: 'AboutView' })
// 在点击时加载组件
const AsyncComponent = defineAsyncComponent(() =>
loadWithDelay(import('@/components/AsyncComponent.vue'), 2000, 'AsyncComponent')
)
const showAsyncComponent = ref<boolean>(false)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
正确的修改:
<Suspense v-if="showAsyncComponent">
<template #fallback>Loading number...</template>
<async-component :id="123"></async-component>
</Suspense>
2
3
4
# Suspense和defineAsyncComponent异步加载的组件优化
<button @click="showAsyncComponent = !showAsyncComponent">showAsyncComponent</button>
<Suspense v-if="showAsyncComponent">
<template #fallback>Loading number...</template>
<async-component
:number="number"
@loaded="isLoaded = true"
>
</async-component>
</Suspense>
2
3
4
5
6
7
8
9
上面的代码,点击按钮控制异步组件的显示和隐藏,由于用v-if控制Suspense,每次点击后都会导致asyc-component组件重新加载。
如何在第一次加载异步组件后,后续的显示与隐藏不再重新加载
- async-component用v-show控制
- Suspense在v-if只执行一次
<Suspense v-if="isLoaded || showAsyncComponent">
<template #fallback>Loading number...</template>
<async-component
:number="number"
v-show="showAsyncComponent"
@loaded="isLoaded = true"
></async-component>
</Suspense>
2
3
4
5
6
7
8
前面讲到通过异步组件@loaded="isLoaded = true"来减少re-render,看官方文档SuspenseProps后发现其实还可以通过onResolve事件来实现这一效果
<Suspense
@resolve="isLoaded = true"
v-if="isLoaded || showAsyncComponent">
<template #fallback>Loading number...</template>
<async-component
v-show="showAsyncComponent"
:number="number"
>
</async-component>
</Suspense>
2
3
4
5
6
7
8
9
10
这里需要注意的一个点是async- component组件内必须有一个根节点,这样v-show才能作用在根节点(v-show 的机制是通过操作根节点的 CSS 样式(display: none)来控制组件的显示和隐藏。)
# React lazy 对比 Vue defineAsyncComponent
相比 React lazy,Vue 3 的 defineAsyncComponent 提供更多控制:
const AsyncComponent = defineAsyncComponent({
loader: () => import("./AsyncComponent.vue"),
loadingComponent: LoadingComponent, // 加载时显示的组件
errorComponent: ErrorComponent, // 发生错误时显示的组件
delay: 200, // 200ms 后才显示 loading
timeout: 3000, // 3秒后如果还未加载则报错
onError(error, retry, fail, attempts) {
if (attempts < 3) {
retry(); // 失败时重试3次
} else {
fail(); // 彻底失败
}
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
# Teleport
Vue的Teleport和React的createProtal功能类似,可以把dom结构插入到指定的位置
# attrs、useAttrs
Attributes 继承
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props (opens new window) 或 emits (opens new window) 的 attribute 或者 v-on 事件监听器。
非props、emits属性会被自动继承到组件的根元素
<span :="{ ...$attrs }">
<span v-bind="{ ...$attrs }">
<span :="{ ...attrs }">
<script setup lang="ts">
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
2
3
4
5
6
7
8
{ ...$attrs }和React的 { ...props }是相同的作用。
# LRU 缓存策略
class LRUCache {
private maxSize: number;
private cache: Map<string, any>;
constructor(maxSize: number) {
this.maxSize = maxSize;
this.cache = new Map();
}
set(key: string, value: any) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size === this.maxSize) {
// 删除最不常用的项
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
get(key: string) {
if (!this.cache.has(key)) {
return undefined;
}
// 将访问过的项移到末尾,表示它是最近使用的
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
}
const lruCache = new LRUCache(10); // 设置最大缓存大小为10
// 设置缓存
lruCache.set('PDFViewer', componentPromise);
// 获取缓存
const cachedComponent = lruCache.get('PDFViewer');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 动态绑定多个值
let addForm = reactive<AddFormType>({
name: '',
age: 0
})
<TestComponent v-bind="addForm" />
2
3
4
5
6
在子组件内怎么接收并使用呢
type TestProps = {
info: AddFormType
name: string
age: number
}
const props = defineProps<TestProps>()
2
3
4
5
6
注意⚠️:一定要通过类型声明来显式接收变量后方可使用对应props
# 通过props传递组件和通过插槽传递组件
props传递组件:
<TestComponent
:default="AsyncComponent"
:info="addForm"
@update:info="updateInfo"
v-bind="addForm"
v-on="getData"
>
<template v-slot:default>
<div>slot default</div>
</template>
</TestComponent>
2
3
4
5
6
7
8
9
10
11
对比React(通过props和props.children)
<TestComponent component={<AsyncComponent />}>
<AsyncComponent />
</TestComponent>
export default function TestComponent({ component }: TestComponentProps) {
return (
<>
<h3>TestComponent</h3>
{component}
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
子组件
<div>
<component :is="default" v-if="info.age > 25" />
</div>
<script setup lang="ts">
type TestProps = {
default: Component
}
</script>
2
3
4
5
6
7
8
9
# 将props传递给通过RouterView显示的组件
使用 v-slot 来实现
<RouterView v-slot="{ Component }">
<component :is="Component" :content="content"></component>
</RouterView>
2
3
# defineSlots
defineSlots<{
default(props: { message: string }): any
}>()
2
3
<TestComponent
:default="AsyncComponent"
:info="addForm"
@update:info="updateInfo"
v-bind="addForm"
v-on="getData"
>
<template v-slot:default="slotProps">
<p>Here is a message from parent: {{ slotProps.message }}</p>
<AsyncComponent :number="199" />
</template>
</TestComponent>
2
3
4
5
6
7
8
9
10
11
12
slotProps是插槽传递的数据
类型声明
defineSlots<{
default(props: { message: string }): any
}>()
2
3
# 通过app.component全局注册的组件没有编辑器提示
全局注册的组件,由于没有在文件内import,导致lsp服务无法分析组件,故而没有编辑器提示
需要手动扩展Vue 的 GlobalComponents 类型
在项目的 src 目录下创建一个新的文件(通常命名为 global-components.d.ts 或类似名称),用于声明全局组件类型。
declare module '@vue/runtime-core' {
interface GlobalComponents {
AsComponent: typeof import('../components/AsyncComponent.vue')['default']
}
}
2
3
4
5
# 符号链接导致的路径问题
但是有个问题,当我们的项目是由pnpm创建的时候,node_modules下没有@vue/runtime-core这个目录
取而代之的是.pnpm目录下的符号链接
解决办法:
使用 TypeScript Paths 显式声明路径
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@vue/runtime-core": [
"./node_modules/.pnpm/@vue+runtime-core@3.5.3/node_modules/@vue/runtime-core/dist/runtime-core.d.ts"
]
}
}
}
2
3
4
5
6
7
8
9
10
# 动态样式
<span
v-for="(item, index) in content"
:class="{
[$style.active]:
audioCurrentTime &&
audioCurrentTime >= item.time[0] &&
audioCurrentTime <= item.time[1],
[$style.vad]: true
}"
:key="index"
:data-wb="item.time[0]"
:data-we="item.time[1]"
>{{ item.text }}
</span>
<style lang="scss" module>
.active {
color: #fff;
background-color: #4499ff;
}
.vad {
transition:
background-color 0.05s ease,
color 0.05s ease;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这里的[$style.active]是因为使用了cssModule
如果不使用cssModule,而是scoped
<span
:class="{
'audio-item cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap': true,
'is-active': homeContext.state.currActiveAudioId === item.id
}"
@click="chooseAudioRecord(item.id)"
>{{ item.audioName }}
</span>
2
3
4
5
6
7
8
还可以使用数组形式
<span
:class="[
'audio-item cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap',
item.status === AudioStatus.FAILED ? 'failed' : ''
]"
@click="chooseAudioRecord(item.id)"
:title="item.audioName"
>
{{ item.audioName }}
</span>Ï
2
3
4
5
6
7
8
9
10
# 给一个组件设置class
在 Vue3 中,当你给一个组件设置 class 属性时,这个属性最终会被应用到组件的根元素上。
如果组件只有一个根元素,则class会被自动应用到根元素上;反之如果组件有多个根元素,Vue3 不会自动决定将 class 应用到哪个元素上。你需要在组件中使用v-bind="$attrs"显式指定哪个元素接收这些属性
# v-bind="$attrs"是干什么的
v-bind="$attrs"是 Vue 3 中实现 “属性透传”(Attributes Inheritance)的终极武器。它能把你传递给父组件的所有属性、事件、Class 和 Style,一键转交给子组件中的某个特定元素。
在 Vue 3 中,$attrs 包含了:
- 你没有在
props中声明的属性(如data-v-xxx,title,name等)。 - 你没有在
emits中声明的事件(如@click,@input等,在$attrs中表现为onClick,onInput)。 - Class 和 Style。
当你写 v-bind="$attrs" 时,就相当于说:“把所有传给我的、我不认识的东西,全部原封不动地交给这个内部元素。”
# 为什么要用它?(核心场景)
假设你在封装一个自己的 MyInput.vue 组件,内部包裹了一个 el-input。
如果你不用 $attrs,你想支持 placeholder、disabled、type、clearable 等几十个属性,你就得在 props 里一个一个写一遍,非常繁琐。
<!-- MultiRootComponent.vue -->
<template>
<header>标题</header>
<main v-bind="$attrs">内容</main>
<footer>页脚</footer>
</template>
2
3
4
5
6
# 禁用属性继承
默认情况下,Vue 会尝试把属性挂在组件的根节点上
如果不希望组件根元素继承class属性,可以给组件设置inheritAttrs: false
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
</script>
2
3
4
5
# v-bind="props"
只包含你在 defineProps 里明确声明的属性。
而v-bind="$attrs"
包含你在 props 和 emits 之外的所有东西。
# useTemplateRef
<button ref="buttonRef" @click="getRef">getRef</button>
<script>
const templateRef = useTemplateRef('buttonRef')
const getRef = () => {
console.log(templateRef.value)
}
</script>
2
3
4
5
6
7
8
# 定义组件ref
const moultiModalQaRef = ref<InstanceType<typeof MoultiModalQa>>()
这样定义的moultiModalQaRef使用起来就有类型提示
← Vue3是如何变快的 Vue3开发准备工作 →